本节内容主要根据 CSS 的发展过程中出现的各种问题、罗列 CSS 各种处理框架的思路。
是对可扩展 CSS 的演变的翻译(在一些无关紧要的字眼上存在部分表述上的变更)
最初#
在 CSS 出现之前及早期,HTML 的样式的控制很朴素,通过少数几个属性进行设置。
<body>
<p SIZE="8" COLOR="RED">red text</p>
</body>
相比于现在的各种 fancy
的页面。那时候的界面十分朴素。除了设置本身的选项少之外,也完全不可复用。只能不断地Reapet Yourself
CSS 的引入#
在受够了 Reapet Yourself
带来的冗杂之后,CSS 进入了游戏,让样式与内容通过一种名为选择器
的东西关联起来,就像许多从以前的痛点中诞生的创新一样,我们现在可以消除所有这些重复。
样式表使我们能够用声明式的范式调整页面样式,用少量代码影响大量元素的样式。现在我们提倡:关注点分离
。
这一次分离做得很彻底,我们现在可以分别考虑内容的结构及其视觉外观和布局。将布局关注点从 HTML 移动到 CSS。
缺陷的显现#
在解决了重复的问题之后,scaling law
继续发挥作用, 规模继续扩张,构建内容的复杂度也越来越高。
我们发现,编写各种选择器本身,也还是存在大量的重复。而且选择器的范围并不容易划定。在最佳实践出现之前,任何新技术通常都需要几个不同方法的周期。
我们看到像 Less 和 Sass 这样的工具弹出来,它们扩展了原生 CSS 功能。为我们提供了变量和 calc 函数等功能,极大地改善了开发人员的体验。
随着我们在这些样式表上花费了更多的时间,我们寻找方法来组织所有这些规则和选择器。
出现了许多不同的 CSS scaling
模式。这些模式试图在可维护性、性能和可读性之间取得一个微妙的平衡,被称为 “CSS 架构”。
在我们深入研究这些架构之前,让我们先了解下为什么在大型项目中管理 CSS 很快就会变得复杂。
为什么 CSS 难以扩展#
这里的 “可扩展” 是指多种事物的交集,包括人员、工具、流程和绩效。搞笑扩展需要我们将复杂性的增长控制可接受的范围内。也就是随着系统的发展,它仍然是可理解的、可变的和高性能的。添加新内容的成本保持尽可能低,同时人们有信心更改和删除旧内容。
级联(CSS 中的 C)起源于 Web 的早期。浏览器可以将默认样式应用于这些新的电子文档。而文档作者可以提供自己的样式,这些样式可以被各个用户首选项覆盖。
这个级联规则是理解 CSS 的核心。它作为 CSS 的强力工具的同时,也是阻止项目进一步扩大的掣肘。特别是其全局命名空间、级联规则和选择器的特异性。
全局命名空间#
如果谨慎利用,全局 CSS 命名空间可以非常强大。但在大型项目中,如通其他编程语言中的全局变量一般,这通常是一种诅咒。
当一切都是全局的时,任何样式都可能意外地影响其他样式。无论是现在还是将来的某个时候,当事情发生变化时。
这很快就会成为问题(其他语言的所有内容放在全局命名空间中是有原因的)。随着添加更多代码,事情变得难以预测且难以维护。
值得注意的是,CSS 级联层是一个新兴的功能,可以通过原生方式解决这个问题。
命名事物很难#
使用 CSS 快速迭代时,创建一系列语义类名通常感觉像是一件苦差事,但并没有太大的好处。
想出有用的名称很困难,因为我们试图将一堆信息压缩成一个精确的标签。当一切都是全局的时候,做到这一点就变得更加重要。
过早命名是一种过早的抽象形式。因为通常我们命名的事物仍然还未成型,也不能重用。
设计更改在前端很常见,样式和标签经常过时,需要重构。
重构 CSS 很困难#
现代软件的设计和开发都是快速迭代的。我们通常只有在几次迭代积累之后才逐渐形成较为清晰的画面。
这需要我们定期重新评估我们对所要解决问题的理解。这意味着代码会随着我们理解的变化 / 固化而进行重构。
重构可能具有挑战性,但这是一种久经考验的真实方法,可以随着时间的推移根据实际需求而不是理论得出良好的抽象。
而在 CSS 中,这非常困难。如果没有可靠的视觉回归测试,许多 CSS 错误是 “无声的”,很容易产生不可预见的错误和副作用。这导致了几种常见的情况。
- Append only stylesheets 仅附加样式表
项目从看起来易于管理的样式表开始。但在经过几次迭代和错误修复后,新代码放在文件末尾是很常见的。
而要知道我们何时可以安全地更改或删除规则是很困难的。因此,我们在文件末尾的级联中覆盖之前的内容。
这就是特异性之争的原因。我们可能都有过需要覆盖其他样式的经历。这是一条通往黑魔法的捷路,!important
开始出现,但实践发现这只是进一步加重了维护负担。 - Dead code 死代码
在实践中,我们经常重复使用相同的 CSS 属性。通常,相比于冒着在全局命名空间中重构大量 CSS 的风险,不断复制规则要安全得多。
这通常会导致大量未使用的 CSS,很难知道是否有东西依赖于它。这最终会使 CSS 膨胀,分布在各种文件中。
调试 CSS 很困难#
调试的很大一部分是模拟计算机在我们脑海中执行的操作。
使用复杂的 CSS 进行调试很困难,因为我们在考虑源顺序的同时,也在脑海中计算级联并计算最终规则。
特别是 CSS 在定位、对齐、堆叠上下文、边距和高度方面的许多细微差别。在没有系统性的方法论的情况下,常见的 CSS 调试工作流程通常是调整一些值,刷新页面以查看会发生什么,得到没有任何变化,或者有什么不起作用的结论。
当使用你无法控制的代码或特定于浏览器的错误时,这尤其具有挑战性。
使用 CSS 架构驯服复杂性#
CSS 有一个简单的模型,但很容易在短时间内就变得混乱。我们最终开始寻求应用软件工程原则来帮助我们管理。这些体系结构更像是组织 CSS 文件、规则和选择器的高级蓝图。
让我们快速了解一些更有影响力和流行的 CSS 架构及其主要思想。
OOCSS: Object Orientated CSS#
OOCSS
(面向对象的 CSS)区分了我们在实践中编写的不同类型的 CSS。
- 进行布局的 CSS
- 主题 HTML(如颜色、字体等)的 CSS
- other...
OOCSS 中的 “对象” 是一种可以抽象和重用的重复视觉模式。其思路是识别常见的视觉模式并将重复的代码块提取到可重用的类中。
一种应用这种思路且广泛使用的 CSS 框架是 Bootstrap。
SMACSS: Scalable and Modular CSS#
在实践中,不用太久,单个大型 CSS 文件就会变得难以管理和调试。
SMACSS(可扩展和模块化的 CSS)是对不同类型的 CSS 进行分类的指南,与 OOCSS 等方法兼容。主要思路是获取所有类名,并将它们到不同的 css 文件中。除了有关类命名的规约之外,还为我们的 CSS 文件提供一些常用的结构。
BEM: Block, Element, Modifier#
BEM(块、元素、修饰符) 是一个思考模型,用于考虑如何将事物拆分为组件、子元素以及各种离散的状态。
它最初由 Yandex 创建,提供了一种系统性的命名规约,通过保持所有选择器不变(没有后代选择器)来规避选择器特异性问题,其中每个被设置样式的元素都有自己的类名。
BEM 与流行的 CSS 预处理器(如 Sass)很好地融合在一起,这些预处理器具有嵌套规则,可以编译为展平的 CSS 选择器:
.nav {
/* block styles */
&__link {
/* element styles that depend on the parent block */
&--active {
/* modifer styles */
}
}
}
ITCSS: Inverted Triangle#
ITCSS(倒三角 CSS) 背后的主要思想之一是通过分层的视角来看待样式表,从而解决级联问题。
ITCSS 是一个类似于 CSS 的 “元框架”,与其他系统兼容。
这个想法是通过提供越来越具体的明确层次来规避一切不可预测地相互重叠的混乱。
“倒三角形” 形成于倒金字塔形状的每个渐进层。
它是在大型项目中管理 CSS 文件的一种较为有影响力的范式。要深入了解,您可以查看其创建者的演讲。
Cube CSS#
Cube CSS 尝试利用好全局命名空间和级联规则。
Cube CSS 提供了一组定义明确的分类:Composition
、Utility
、Block
、Exception
。共同构成cube
的首字母缩略词。
这些文档在解释原则方面做得很好。是一种相对宽松的方法论,类似于一个组织 CSS 的心智模型。
与 ITCSS 类似,它是一个有影响力的 “元 CSS 框架”,与各种方法兼容。
重新思考关注点分离
#
随着 SPA 和组件驱动开发的兴起,我们开始看到 CSS 的新范式。
在这个世界里,管理 CSS 变得更加困难,因为组件现在是异步加载的,因此无法保证按源的顺序加载。
一个常见问题是,在执行从页面 A 到 B 的 SPA 转换时,页面上的某些元素看起来不同,但如果直接加载到 B,则看起来还不错。
我们开始寻找更具体的解决方案来管理 CSS。寻找能够与以组件为中心范式相结合,一起构建我们的前端页面的解决方案。
大多数这样的工具都打破了我们迄今为止一直在思考和建立的许多既定最佳实践。让我们了解一下他们。
Inline styles 内联样式#
向基于组件的框架的转变后,经常看到样式内联于组件内部。在像 React 这样的框架中,我们将一个 Javascript 对象传递给 style
prop,将其转换为内联样式。
这引起了许多人的本能厌恶,因为这似乎让我们再次回到了没有CSS
的时候,抛弃了现有的最佳实践。
在组件的上下文中,内联样式不会面临最初的大量重复问题,因为它被封装在组件内部。“样式只影响它们所在的元素” 这一事实使得内联样式是在组件中安全地添加和修改 CSS 的好方法。
除了难以利用共享设计令牌、缓存、静态分析和 CSS 预处理器外,内联样式的主要问题是无法访问更强大的 CSS 功能,例如伪选择器和媒体查询。
CSS in JS #
在 React 的早期,Veujx 发表了关于Facebook 的 CSS 范式 的演讲。从表面上看,这看起来很像内联样式,但可以利用样式表的强大功能。
这次演讲促进了 CSS 开源库的激增,这些库采用了 JavaScript 驱动 CSS 的范式。
第一波 CSS In JS
库开始流行起来(如 Styled Components、Emotion 以及 React 生态系统中的一系列其他库)。
这些库解决了原生 CSS 在组件驱动的大型项目中遇到的大部分问题,使用 JS 中的动态值变得非常容易。
问题在于最终用户要支付的性能税:服务端渲染效率低下、缓存问题和客户端运行时成本。
这进一步增加了本就缓慢的应用程序启动速度。JS 水合之后,还需要多次重新渲染。尤其是大型应用程序需要大量准备工作的情况下,这一点更是致命。
最近(原文写于 2022 年末)的第二波 CSS In JS
浪潮旨在消减运行时成本的前提下,提供最佳的开发人员体验。
像Vanilla extract、Linaria 和 Compiled 这样的工具,在编译步骤中从组件中提取出样式表。这可以将用户浏览器运行时发生的大部分操作移至编译时。组件中提取出的样式通常被编译成原子 CSS(我们稍后会谈到的一种 CSS 架构),以避免 CSS 文件太大,且与运行时动态 CSS 相比,它的可缓存性要高得多。
CSS Modules#
模块化 CSS 在编写常规 CSS(或 Sass)和实现我们正在寻找的可扩展 CSS 之间取得了平衡。
模块化 CSS 可以使用 CSS 的全部功能,同时避免样式效果溢出到组件之外,还能让 CSS 与组件保持合适的距离(既不远,也不太近)。
特别是在第一波 CSS In JS
浪潮中,将 CSS 绑定到特定的视图库对一些人来说这太过分了。而模块化 CSS 是一个很好的选择。不过,也有观点认为这是 CSS In JS
的另一种形式,因为它依赖于打包器(如 Webpack)确保选择器的不外溢以及最终 CSS 的生成。
无论如何,CSS 模块是常规 CSS 世界和完全以组件驱动的范式(如 CSS In JS
)之间的一个很好的中间地带。命名仍然是必需的,可以与 BEM 等约定兼容。
具有挑战性的 CSS 最佳实践#
Meanwhile, outside the world of component-based SPA’s, the original CSS Zen Garden influenced best practices were challenged on another front.
与此同时,在基于组件的 SPA 世界之外,最初受 CSS Zen Garden 影响的最佳实践在另一个方面受到了挑战。
原子化 CSS 诞生于在大型项目管理 CSS 的黑暗中,也由它塑造。
最初的动机是完全基于现实主义的 —— 无需编辑或将规则附加到现有样式表即可样式组件,从而避免随之而来的所有问题。
与 OOCSS、BEM 和 SMACSS 等其他 CSS 架构相比,它处于光谱的另一极端,完全违反直觉。
原子 CSS 比 “blocks” 和 “objects” 低一个级别,专注于单一用途的原子。直接违背了 HTML 规范中关于如何不命名 CSS 类的既定最佳实践。
对于处理 / 修改现有 CSS 感觉风险太大的项目团队来说,它已成为一种流行的提高生产力的方法。一些流行的 CSS 库包括 ACSS、Tachyons、WindiCSS 等。
根据 state of CSS 的调查,这种 CSS 架构最流行的实现之一是 Tailwind CSS 框架。
Tailwind 的崛起#
自 2017 年发布以来,Tailwind 迅速获得了广泛的关注。Tailwind 的一个典型评价是,它通过使 CSS 对非专家人员更友好,并使得 CSS 更加易于维护,从而提高生产力。多数评价是 “试试吧,你不会后悔的”。
Tailwind 的动力原则#
为了了解它的受欢迎程度,让我们来看看 Tailwind 方法背后的基本原理。尽管看似抛弃了既定的最佳实践,但我们将看到其背后是一系列在实践中行之有效的务实原则。
推迟命名#
不必不断命名是 Tailwind 让人感觉如此高效的一个重要原因。该工作流以自下而上组合原子化样式的理念为后盾。从可维护性的角度来看,这是避免仓促抽象的好方法。
不命名会损害代码的可读性,通常会导致一堆没有明确边界的原子类(或组件)。这是一个很有道理的批评。但在实践中,在 “匿名” 成为痛点之前,这条路意外的能走很远。在代码库经常更改或有很多人的情况下,这通常是值得的。
这是组件模型中一个强大的工作流。你已经为你正在使用的组件提供了一个具有语义的名称,所以你最终会朝着这个方向构建,并将样式包含在组件中,命名的责任从样式中转移到组件。
这与构建面向未来的前端中概述的更高级别原则相同。这并不是说永远不创建抽象,而是不要过早地创建它们。
在适当的时候进行抽象#
迭代时,轻松删除和更改代码的能力大大超过了复制的成本。
编写 CSS 时的一个常见痛点是,当一些样式不轻易改变时,预先让事情变得过于 DRY 或过早优化通常会感觉像是一种浪费。
在重复代码到一定程度后再进行 DRY 通用抽象将其用于新用例要比解决过度抽象的代码要容易得多。
Tailwind 提供了两种解决方案,从而可以在正确的时间进行抽象。
- 通过创建的共享 CSS 类来表示块(类似于 OOCSS)。
- 或者,在使用基于组件的框架时,更鼓励将重复的类提取到可重用的(React、Vue 等)组件中并共享它。
放心去重构#
因为样式类被局部化到它们所在的 label,所以我们可以放心地重构它们,而不必担心影响其他地方的其他元素或组件。
这既适用于 Web 作为文档的心智模型,也适用于以组件为中心的模型。这使得 Tailwind 可以根据你正在构建的网站或应用程序的类型进行扩展或收缩。
避免无用代码#
Tailwind 和其他预编译为原子化 CSS 的Css In JS
库解决了充满重复规则的臃肿 CSS 文件。
使用原子化 CSS 时,CSS 的增长与所使用的独特样式的数量有关,而非开发人员提供的功能数量。
例如,在任何地方重用某些属性(如 flex
)是很常见的。与其在不同的类名下的样式表中复制这些,我们只需要一次性的代价即可随意使用。对于每个属性 / 值的组合都是如此。
弥合设计差距#
让我们从所有这些原则和架构中抽身出来,要知道,CSS 最终是关于实现视觉设计的。
CSS 对许多开发人员来说感觉困难的一个重要原因是设计很难。
掌握正确的基本原理大有帮助。在视觉设计中,关键元素是对齐、间距、一致性、大小、排版和颜色。
在 CSS 中,对于任何给定的属性,如 font-size
、color
或 padding
,都有多种方法可以实现。
通常,我们以一种临时的方式执行此操作,这会导致字体大小、间距和颜色略有不同,,这些不一致叠加起来会导致最终结果看起来未经修饰。
可拓展 CSS 的一个关键是通过拥有 “能定义间距、字体大小、颜色、断点等值的可共享原语” 的坚实基础来弥合这一设计差距。
也即Desigin Token,它是设计系统的基础。没有这个基础,通常会让人感觉非常随意、混乱。
Tailwind 受欢迎的一个关键方面是提供了一组现成的预定义基础设计原语。这消除了大量决策(这些决策通常是临时完成的,很难保持一致)。
另一个很好的开源替代是 Open Props,它与你采用的任何 CSS 方法兼容,并提供了大量很棒的预定义变量和原语。
结束语#
”Absorb what is useful, discard what is useless, and add what is specifically your own” - Bruce Lee
“取其所长,弃其所短,添我所能” - 李小龙
没有工具是完美的,每个项目和团队都是不同的。无论采用哪种方法,拥有弥合设计差距的基础才是扩展 CSS 的关键要素。
专注于在其上组合和构建的原语也有很长的路要走。这也适用于使用组件库的大型基于组件的应用程序。提供 Box
、Stack
、Inline
等可组合的布局原语组件是管理 CSS 的好方法,开发人员无需编写任何 CSS。
最近几年,浏览器中新增了大量特性,解决了许多阻碍 CSS 进行扩展的痛点。级联层、容器查询、子网格、has
等新功能可能会改变我们未来思考和利用 CSS 的方式。
要在可扩展 CSS 上取得成功,更多依赖于根据现实世界的规约来定义自己的需求,而非教条地遵循某些原则或最佳实践。关键是采取高效且可持续的做法,确保任务顺利完成。